/*
 *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc.voiceengine;

import android.media.audiofx.AcousticEchoCanceler;
import android.media.audiofx.AudioEffect;
import android.media.audiofx.AudioEffect.Descriptor;
import android.media.audiofx.NoiseSuppressor;
import android.os.Build;

import androidx.annotation.Nullable;

import java.util.List;
import java.util.UUID;

import org.webrtc.Logging;

// This class wraps control of three different platform effects. Supported
// effects are: AcousticEchoCanceler (AEC) and NoiseSuppressor (NS).
// Calling enable() will active all effects that are
// supported by the device if the corresponding |shouldEnableXXX| member is set.
public class WebRtcAudioEffects {
    private static final boolean DEBUG = false;

    private static final String TAG = "WebRtcAudioEffects";

    // UUIDs for Software Audio Effects that we want to avoid using.
    // The implementor field will be set to "The Android Open Source Project".
    private static final UUID AOSP_ACOUSTIC_ECHO_CANCELER =
            UUID.fromString("bb392ec0-8d4d-11e0-a896-0002a5d5c51b");
    private static final UUID AOSP_NOISE_SUPPRESSOR =
            UUID.fromString("c06c8400-8e06-11e0-9cb6-0002a5d5c51b");

    // Contains the available effect descriptors returned from the
    // AudioEffect.getEffects() call. This result is cached to avoid doing the
    // slow OS call multiple times.
    private static @Nullable
    Descriptor[] cachedEffects;

    // Contains the audio effect objects. Created in enable() and destroyed
    // in release().
    private @Nullable
    AcousticEchoCanceler aec;
    private @Nullable
    NoiseSuppressor ns;

    // Affects the final state given to the setEnabled() method on each effect.
    // The default state is set to "disabled" but each effect can also be enabled
    // by calling setAEC() and setNS().
    // To enable an effect, both the shouldEnableXXX member and the static
    // canUseXXX() must be true.
    private boolean shouldEnableAec;
    private boolean shouldEnableNs;

    // Checks if the device implements Acoustic Echo Cancellation (AEC).
    // Returns true if the device implements AEC, false otherwise.
    public static boolean isAcousticEchoCancelerSupported() {
        // Note: we're using isAcousticEchoCancelerEffectAvailable() instead of
        // AcousticEchoCanceler.isAvailable() to avoid the expensive getEffects()
        // OS API call.
        return isAcousticEchoCancelerEffectAvailable();
    }

    // Checks if the device implements Noise Suppression (NS).
    // Returns true if the device implements NS, false otherwise.
    public static boolean isNoiseSuppressorSupported() {
        // Note: we're using isNoiseSuppressorEffectAvailable() instead of
        // NoiseSuppressor.isAvailable() to avoid the expensive getEffects()
        // OS API call.
        return isNoiseSuppressorEffectAvailable();
    }

    // Returns true if the device is blacklisted for HW AEC usage.
    public static boolean isAcousticEchoCancelerBlacklisted() {
        List<String> blackListedModels = WebRtcAudioUtils.getBlackListedModelsForAecUsage();
        boolean isBlacklisted = blackListedModels.contains(Build.MODEL);
        if (isBlacklisted) {
            Logging.w(TAG, Build.MODEL + " is blacklisted for HW AEC usage!");
        }
        return isBlacklisted;
    }

    // Returns true if the device is blacklisted for HW NS usage.
    public static boolean isNoiseSuppressorBlacklisted() {
        List<String> blackListedModels = WebRtcAudioUtils.getBlackListedModelsForNsUsage();
        boolean isBlacklisted = blackListedModels.contains(Build.MODEL);
        if (isBlacklisted) {
            Logging.w(TAG, Build.MODEL + " is blacklisted for HW NS usage!");
        }
        return isBlacklisted;
    }

    // Returns true if the platform AEC should be excluded based on its UUID.
    // AudioEffect.queryEffects() can throw IllegalStateException.
    private static boolean isAcousticEchoCancelerExcludedByUUID() {
        if (Build.VERSION.SDK_INT < 18)
            return false;
        for (Descriptor d : getAvailableEffects()) {
            if (d.type.equals(AudioEffect.EFFECT_TYPE_AEC)
                    && d.uuid.equals(AOSP_ACOUSTIC_ECHO_CANCELER)) {
                return true;
            }
        }
        return false;
    }

    // Returns true if the platform NS should be excluded based on its UUID.
    // AudioEffect.queryEffects() can throw IllegalStateException.
    private static boolean isNoiseSuppressorExcludedByUUID() {
        if (Build.VERSION.SDK_INT < 18)
            return false;
        for (Descriptor d : getAvailableEffects()) {
            if (d.type.equals(AudioEffect.EFFECT_TYPE_NS) && d.uuid.equals(AOSP_NOISE_SUPPRESSOR)) {
                return true;
            }
        }
        return false;
    }

    // Returns true if the device supports Acoustic Echo Cancellation (AEC).
    private static boolean isAcousticEchoCancelerEffectAvailable() {
        if (Build.VERSION.SDK_INT < 18)
            return false;
        return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_AEC);
    }

    // Returns true if the device supports Noise Suppression (NS).
    private static boolean isNoiseSuppressorEffectAvailable() {
        if (Build.VERSION.SDK_INT < 18)
            return false;
        return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_NS);
    }

    // Returns true if all conditions for supporting the HW AEC are fulfilled.
    // It will not be possible to enable the HW AEC if this method returns false.
    public static boolean canUseAcousticEchoCanceler() {
        boolean canUseAcousticEchoCanceler = isAcousticEchoCancelerSupported()
                && !WebRtcAudioUtils.useWebRtcBasedAcousticEchoCanceler()
                && !isAcousticEchoCancelerBlacklisted() && !isAcousticEchoCancelerExcludedByUUID();
        Logging.d(TAG, "canUseAcousticEchoCanceler: " + canUseAcousticEchoCanceler);
        return canUseAcousticEchoCanceler;
    }

    // Returns true if all conditions for supporting the HW NS are fulfilled.
    // It will not be possible to enable the HW NS if this method returns false.
    public static boolean canUseNoiseSuppressor() {
        boolean canUseNoiseSuppressor = isNoiseSuppressorSupported()
                && !WebRtcAudioUtils.useWebRtcBasedNoiseSuppressor() && !isNoiseSuppressorBlacklisted()
                && !isNoiseSuppressorExcludedByUUID();
        Logging.d(TAG, "canUseNoiseSuppressor: " + canUseNoiseSuppressor);
        return canUseNoiseSuppressor;
    }

    public static WebRtcAudioEffects create() {
        return new WebRtcAudioEffects();
    }

    private WebRtcAudioEffects() {
        Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
    }

    // Call this method to enable or disable the platform AEC. It modifies
    // |shouldEnableAec| which is used in enable() where the actual state
    // of the AEC effect is modified. Returns true if HW AEC is supported and
    // false otherwise.
    public boolean setAEC(boolean enable) {
        Logging.d(TAG, "setAEC(" + enable + ")");
        if (!canUseAcousticEchoCanceler()) {
            Logging.w(TAG, "Platform AEC is not supported");
            shouldEnableAec = false;
            return false;
        }
        if (aec != null && (enable != shouldEnableAec)) {
            Logging.e(TAG, "Platform AEC state can't be modified while recording");
            return false;
        }
        shouldEnableAec = enable;
        return true;
    }

    // Call this method to enable or disable the platform NS. It modifies
    // |shouldEnableNs| which is used in enable() where the actual state
    // of the NS effect is modified. Returns true if HW NS is supported and
    // false otherwise.
    public boolean setNS(boolean enable) {
        Logging.d(TAG, "setNS(" + enable + ")");
        if (!canUseNoiseSuppressor()) {
            Logging.w(TAG, "Platform NS is not supported");
            shouldEnableNs = false;
            return false;
        }
        if (ns != null && (enable != shouldEnableNs)) {
            Logging.e(TAG, "Platform NS state can't be modified while recording");
            return false;
        }
        shouldEnableNs = enable;
        return true;
    }

    public void enable(int audioSession) {
        Logging.d(TAG, "enable(audioSession=" + audioSession + ")");
        assertTrue(aec == null);
        assertTrue(ns == null);

        if (DEBUG) {
            // Add logging of supported effects but filter out "VoIP effects", i.e.,
            // AEC, AEC and NS. Avoid calling AudioEffect.queryEffects() unless the
            // DEBUG flag is set since we have seen crashes in this API.
            for (Descriptor d : AudioEffect.queryEffects()) {
                if (effectTypeIsVoIP(d.type)) {
                    Logging.d(TAG, "name: " + d.name + ", "
                            + "mode: " + d.connectMode + ", "
                            + "implementor: " + d.implementor + ", "
                            + "UUID: " + d.uuid);
                }
            }
        }

        if (isAcousticEchoCancelerSupported()) {
            // Create an AcousticEchoCanceler and attach it to the AudioRecord on
            // the specified audio session.
            aec = AcousticEchoCanceler.create(audioSession);
            if (aec != null) {
                boolean enabled = aec.getEnabled();
                boolean enable = shouldEnableAec && canUseAcousticEchoCanceler();
                if (aec.setEnabled(enable) != AudioEffect.SUCCESS) {
                    Logging.e(TAG, "Failed to set the AcousticEchoCanceler state");
                }
                Logging.d(TAG, "AcousticEchoCanceler: was " + (enabled ? "enabled" : "disabled")
                        + ", enable: " + enable + ", is now: "
                        + (aec.getEnabled() ? "enabled" : "disabled"));
            } else {
                Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance");
            }
        }

        if (isNoiseSuppressorSupported()) {
            // Create an NoiseSuppressor and attach it to the AudioRecord on the
            // specified audio session.
            ns = NoiseSuppressor.create(audioSession);
            if (ns != null) {
                boolean enabled = ns.getEnabled();
                boolean enable = shouldEnableNs && canUseNoiseSuppressor();
                if (ns.setEnabled(enable) != AudioEffect.SUCCESS) {
                    Logging.e(TAG, "Failed to set the NoiseSuppressor state");
                }
                Logging.d(TAG, "NoiseSuppressor: was " + (enabled ? "enabled" : "disabled") + ", enable: "
                        + enable + ", is now: " + (ns.getEnabled() ? "enabled" : "disabled"));
            } else {
                Logging.e(TAG, "Failed to create the NoiseSuppressor instance");
            }
        }
    }

    // Releases all native audio effect resources. It is a good practice to
    // release the effect engine when not in use as control can be returned
    // to other applications or the native resources released.
    public void release() {
        Logging.d(TAG, "release");
        if (aec != null) {
            aec.release();
            aec = null;
        }
        if (ns != null) {
            ns.release();
            ns = null;
        }
    }

    // Returns true for effect types in |type| that are of "VoIP" types:
    // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or
    // Noise Suppressor (NS). Note that, an extra check for support is needed
    // in each comparison since some devices includes effects in the
    // AudioEffect.Descriptor array that are actually not available on the device.
    // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but
    // AutomaticGainControl.isAvailable() returns false.
    private boolean effectTypeIsVoIP(UUID type) {
        if (Build.VERSION.SDK_INT < 18)
            return false;

        return (AudioEffect.EFFECT_TYPE_AEC.equals(type) && isAcousticEchoCancelerSupported())
                || (AudioEffect.EFFECT_TYPE_NS.equals(type) && isNoiseSuppressorSupported());
    }

    // Helper method which throws an exception when an assertion has failed.
    private static void assertTrue(boolean condition) {
        if (!condition) {
            throw new AssertionError("Expected condition to be true");
        }
    }

    // Returns the cached copy of the audio effects array, if available, or
    // queries the operating system for the list of effects.
    private static @Nullable
    Descriptor[] getAvailableEffects() {
        if (cachedEffects != null) {
            return cachedEffects;
        }
        // The caching is best effort only - if this method is called from several
        // threads in parallel, they may end up doing the underlying OS call
        // multiple times. It's normally only called on one thread so there's no
        // real need to optimize for the multiple threads case.
        cachedEffects = AudioEffect.queryEffects();
        return cachedEffects;
    }

    // Returns true if an effect of the specified type is available. Functionally
    // equivalent to (NoiseSuppressor|AutomaticGainControl|...).isAvailable(), but
    // faster as it avoids the expensive OS call to enumerate effects.
    private static boolean isEffectTypeAvailable(UUID effectType) {
        Descriptor[] effects = getAvailableEffects();
        if (effects == null) {
            return false;
        }
        for (Descriptor d : effects) {
            if (d.type.equals(effectType)) {
                return true;
            }
        }
        return false;
    }
}
